var fs = require('fs');
var path = require('path');
var async = require('async');
var request = require('request');

var _context;
var _logger;
var _outputPath;
var _idx = 0;
var _requests = [];
var _outputFiles = true;
var _config;

// default output options
var _outputOptions = {
    jsonIndent: 3,
    responseHeaders: ['content-type']
}

var _baseHttpOptions = {
    host: 'localhost',
    port: 80,
    path: '/',
    method: 'GET',
    headers: {
        'Content-Type': 'application/json'
    }
};

/*-----------------------------------------------------------
 *
 * Initialization
 *
 *-----------------------------------------------------------*/

exports.initialize = function(config, writerOptions) {
    _idx = 0;
    _requests = [];
    _config = config;
    _baseHttpOptions = writerOptions.requestOptions;

    if (!fs.existsSync(writerOptions.outputOptions.outputRoot))
        throw 'output root does not exist';    

    _outputPath = path.join(writerOptions.outputOptions.outputRoot, 
        writerOptions.outputOptions.outputFolder);

    if (writerOptions.outputOptions)
        _outputOptions = writerOptions.outputOptions;
}

exports.setLogger = function(logger) {
    _logger = logger;
}

// can temporarily disable output of files
exports.setEnableWrite = function(shouldWrite) {
    _outputFiles = shouldWrite;
}

exports.resetOutput = function() {
    if (!_outputPath)
        throw 'outputPath not set.  Call initialize';

    if (fs.existsSync(_outputPath)) {
        var files = fs.readdirSync(_outputPath);
        files.forEach(function(file,index) {
            var curPath = path.join(_outputPath, file);
            fs.unlinkSync(curPath);
        });
        fs.rmdirSync(_outputPath);
        fs.mkdirSync(_outputPath);
    }
};

/*-----------------------------------------------------------
 * Documentation Methods
 *-----------------------------------------------------------*/

exports.documentResources = function(resourcePath, beginCallback)
{
    var resources = fs.readdirSync(resourcePath);

    async.forEachSeries(resources, function(resourceScript, callback) {
        module.exports.documentResource(resourcePath, resourceScript, beginCallback, callback);
    });
}

exports.documentResource = function(resourcePath, resourceScript, beginCallback, callback) {
    _requests = [];
    _idx = 0;

    if (resourceScript.split('.').pop() != 'js')
        callback(null, "skip");

    var res = require(resourcePath + '/' + resourceScript);
    res.getConfig = function() {
        return _config;
    };

    beginCallback(resourceScript);

    _context = res.getContext();

    res.submitRequests();
    module.exports.documentRequests(_requests, callback);
}

exports.documentRequests = function(tasks, callback) {
    async.forEachSeries(tasks, function(task, asyncCallback) {
        task.process(asyncCallback);
    }, callback);
}

/*-----------------------------------------------------------
 * Request Methods
 *
 * These methods build up a 'request tasks' and it to an array
 * which will be run serially using async.js when the document
 * methods above are called.
 *
 * - xxxJson: takes formated path (/foo/bar/{id}) and an
 *            onResult callback so you can store context state
 *
 * - xxxJsonEx: same as xxxJson except has a prepareRequest
 *              callback if you want to have custom formatting
 *              of url or request.
 *-----------------------------------------------------------*/
exports.getJson = function(relativePathFormat, onResult)
{
    var prepare = _getPrepare(relativePathFormat);
    return module.exports.getJsonEx(relativePathFormat, prepare, onResult);
}

exports.getJsonEx = function(relativePathFormat, prepareRequest, onResult) {
    _queueRequest("GET", relativePathFormat, prepareRequest, onResult);
};

// obj can be an object or a function pointer to build the object
exports.postJson = function(relativePathFormat, obj, onResult)
{
    var prepare = _getPrepare(relativePathFormat);

    var getObjFunc = typeof(obj) == "function"? obj : _getObject(obj);
    return module.exports.postJsonEx(relativePathFormat, getObjFunc, prepare, onResult);
}

exports.postJsonEx = function(relativePathFormat, getObject, prepareRequest, onResult) {
    _queueSendRequest("POST", relativePathFormat, getObject, prepareRequest, onResult);
};

exports.deleteJson = function(relativePathFormat, onResult)
{
    var prepare = _getPrepare(relativePathFormat);
    return module.exports.deleteJsonEx(relativePathFormat, prepare, onResult);
}

exports.deleteJsonEx = function(relativePathFormat, prepareRequest, onResult) {
    _queueRequest("DELETE", relativePathFormat, prepareRequest, onResult);
};

exports.patchJson = function(relativePathFormat, obj, onResult)
{
    var prepare = _getPrepare(relativePathFormat);
    module.exports.patchJsonEx(relativePathFormat, obj, prepare, onResult);
}

exports.patchJsonEx = function(relativePathFormat, obj, prepareRequest, onResult) {
    var getObjFunc = typeof(obj) == "function"? obj : _getObject(obj);
    _queueSendRequest("PATCH", relativePathFormat, getObjFunc, prepareRequest, onResult);
};

exports.putJson = function(relativePathFormat, obj, onResult)
{
    var prepare = _getPrepare(relativePathFormat);
    module.exports.putJsonEx(relativePathFormat, obj, prepare, onResult);
}

exports.putJsonEx = function(relativePathFormat, obj, prepareRequest, onResult) {
    var getObjFunc = typeof(obj) == "function"? obj : _getObject(obj);
    _queueSendRequest("PUT", relativePathFormat, getObjFunc, prepareRequest, onResult);
};

/*-----------------------------------------------------
 *  Private Methods
 *------------------------------------------------------*/
var _queueRequest = function(method, relativePathFormat, prepareRequest, onResult) {
    var newRequest = {};
    var shouldWrite = _outputFiles;

    newRequest.process = function(asyncCallback) {
        options = _copyOptions(method);
        options.writeOutputFiles = shouldWrite;
        options.headers = {};
        options.headers["Accept"] = 'application/json';
        prepareRequest(_context, options);

        request(options, function(err, resp, body){
            var output = _handleRequest(relativePathFormat, options, err, resp, body, onResult);
            asyncCallback(err, output);
        });
    };

    _requests[_idx++] = newRequest;
};

var _queueSendRequest = function(method, relativePathFormat, getObject, prepareRequest, onResult) {
    var newRequest = {};
    var shouldWrite = _outputFiles;

    newRequest.process = function(asyncCallback) {
        options = _copyOptions(method);
        options.writeOutputFiles = shouldWrite;
        options.body = JSON.stringify(getObject(_context, options));
        options.headers = {};
        options.headers["Content-Type"] = 'application/json';
        options.headers["Content-Length"] = options.body.length;

        _outputInfo('post item: ' + JSON.stringify(options.body, null, 2));

        prepareRequest(_context, options);

        request(options, function(err, resp, body){
            var output = _handleRequest(relativePathFormat, options, err, resp, body, onResult);
            asyncCallback(err, output);
        });
    };

    _requests[_idx++] = newRequest;
};

var _handleRequest = function(relativePathFormat, options, err, resp, body, onResult){
    _outputInfo("[" + options.method + "]" + options.url);
    var output = {};
    if (err)
    {
        _outputInfo('Request Failed: ' + err.message);
    }
    else
    {
        if (resp.statusCode >= 200 && resp.statusCode < 300)
        {
            output.method = options.method;
            output.resourceFormat = relativePathFormat;
            output.requestUrl = options.url;
            output.requestHeaders = options.headers;
            if (options.requestBody)
                output.requestBody = options.requestBody;

            output.statusCode = resp.statusCode;
            output.responseHeaders = resp.headers;

            if (body)
                output.responseBody = JSON.parse(body);

            if (options.writeOutputFiles) {
                var fileName = formatFileName(options.method + "_" + relativePathFormat);
                _writeJson(_outputPath, fileName, output);
            }

            if (onResult)
                onResult(_context, output);
        }
        else
        {
            _outputInfo('Request was not successfull.');
            if (resp)
                _outputInfo(resp.statusCode + ': message = ' + resp.message);

            _outputInfo(JSON.stringify(options));
        }
    }

    return output;
}

var _getPrepare = function(relativePathFormat)
{
    // execution of prepare must be deferred to run time of tasks
    // since it relies on output from previous call writing to context
    var prepare = function(context, options) {
        var relativeUrl = formatUrl(relativePathFormat, _context);
        options.url += relativeUrl;
    };
    return prepare;
}

var _getObject = function(obj)
{
    // deferring execution of preparing the body so it can be built based on context
    // at runtime
    var getObject = function(context, options) {
        return obj;
    };
    return getObject;
}

// TODO: copy all options by iterating.  Handle headers effectively
var _copyOptions = function(method) {
    var options = {
        url: _baseHttpOptions.url,
        auth: _baseHttpOptions.auth,
        followRedirect: _baseHttpOptions.followRedirect,
        method: method,
    };

    return options;
}

var _writeJson = function(directory, fileName, result)
{
    var outputPath = path.join(directory, fileName);
    var outputPath = path.normalize(outputPath);
    _outputInfo('writing: ' + outputPath);
    fs.writeFileSync(outputPath, JSON.stringify(result, null, _outputOptions.jsonIndent));
}

var _outputResult = function (httpOptions, statusCode, result)
{
    _outputInfo('statusCode: ' + statusCode + " for " + httpOptions.path);
};

var _outputInfo = function(msg) {
    if (_logger) {
        _logger(msg);
    }
};

var formatUrl = function(formattedUrl, values) {
    var resultUrl = formattedUrl;
    for (key in values)
    {
        var val = values[key];
        if (val)
        {
            resultUrl = resultUrl.replace('{' + key + '}', values[key]);
        }
    }

    return resultUrl;
}

var formatFileName = function(formattedUrl) {
    var fileName = formattedUrl.replace(/{/g, "_");
    fileName = fileName.replace(/}/g, "_");
    fileName = fileName.replace(/\//g, "_");
    fileName = fileName.replace(/\&/g, "_");
    fileName = fileName.replace(/\?/g, "_");
    fileName = fileName.replace(/=/g, "-");
    return fileName + ".json";
}
